{ "cells": [ { "cell_type": "markdown", "id": "e1e2e446", "metadata": {}, "source": [ "This is part 1 of a tutorial series. We recommend following them in order, starting with [Part 0: Welcome to `musica`](0.%20Welcome%20to%20MUSICA.ipynb)." ] }, { "cell_type": "markdown", "id": "7030540f", "metadata": {}, "source": [ "# Multiple Grid Cells in `musica`\n", "\n", "As we saw in the last tutorial, gas-phase systems can be solved in `musica` using a `solver` and a `state`. For box models, that's all we need. For more complex applications (columns, 3D grids, etc.), we'll need to manage the state of a number of independent well-mixed air masses, or \"grid cells\".\n", "\n", ">__NOTE:__ We use \"grid cell\" throughout these tutorials to mean a well-mixed air mass. A vertical stack of \"grid cells\" would be used for a column-model. Collections of vertical stacks of grid cells would be used for a 3D grid or mesh. However, `micm` does not make assumptions about the shape, size, or arragement of the well-mixed air mass(es) a `state` represents, so the state can be used to represent air masses of any size/shape in any arrangement.\n", "\n", "_Why multiple grid cell states?_\n", "\n", "If we can create as many states as we want and use the same solver on them, maybe you're wondering why we even need multiple-grid-cell states. The answer, once again, is for performance. In HPC applications, `micm` can be used to solve hundreds of thousands of independent grid cells simultaneously. To do this efficiently, the data needs to be efficiently arranaged in memory. Multiple-grid-cell states allow us to do this." ] }, { "cell_type": "markdown", "id": "008e870b", "metadata": {}, "source": [ "## 1. Importing Libraries\n", "Below is a list of the required libraries for this tutorial:" ] }, { "cell_type": "code", "execution_count": 1, "id": "7c921c61", "metadata": {}, "outputs": [], "source": [ "import musica\n", "import musica.mechanism_configuration as mc\n", "import matplotlib.pyplot as plt\n", "import pandas as pd\n", "import numpy as np\n", "pd.set_option('display.float_format', str) # This is done to make the arrays more readable\n", "np.set_printoptions(suppress=True) # This is done to make the arrays more readable" ] }, { "cell_type": "markdown", "id": "01472a4f", "metadata": {}, "source": [ "## 2. Defining a System\n", "\n", "In the first tutorial, we used an example configuration file to define our chemical system. This time, we'll use the `musica` API to define a chemical system in code. We'll use this apporach for future tutorials as well.\n", "\n", "Let's set up a system analogous to the simple analytically solvable system we used in the first tutorial" ] }, { "cell_type": "code", "execution_count": 2, "id": "6b1084ee", "metadata": {}, "outputs": [], "source": [ "A = mc.Species(name=\"A\")\n", "B = mc.Species(name=\"B\")\n", "C = mc.Species(name=\"C\")\n", "species = [A, B, C]\n", "gas = mc.Phase(name=\"gas\", species=species)\n", "\n", "r1 = mc.Arrhenius(\n", " name=\"A_to_B\",\n", " A=4.0e-3,\n", " C=50,\n", " reactants=[A],\n", " products=[B],\n", " gas_phase=gas\n", ")\n", "\n", "r2 = mc.Arrhenius(\n", " name=\"B_to_C\",\n", " A=4.0e-3,\n", " C=50, \n", " reactants=[B],\n", " products=[C],\n", " gas_phase=gas\n", ")" ] }, { "cell_type": "markdown", "id": "45b4a60a", "metadata": {}, "source": [ "Similar to the last tutorial, we have defined a simple system with:\n", "* Three species (`A`, `B`, and `C`) in the gas-phase\n", "* Two Arrhenius reactions\n", " * `A -> B`\n", " * `B -> C`\n", "\n", "We try to keep the python API consistent with the format of the configuration files, and have both structured around the science.\n", "\n", "The documentation can be used to clarify important details, like the parameter names and units for rate constant types" ] }, { "cell_type": "code", "execution_count": 3, "id": "81bcef4c", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\u001b[31mInit signature:\u001b[39m\n", "mc.Arrhenius(\n", " name: str | \u001b[38;5;28;01mNone\u001b[39;00m = \u001b[38;5;28;01mNone\u001b[39;00m,\n", " A: float | \u001b[38;5;28;01mNone\u001b[39;00m = \u001b[38;5;28;01mNone\u001b[39;00m,\n", " B: float | \u001b[38;5;28;01mNone\u001b[39;00m = \u001b[38;5;28;01mNone\u001b[39;00m,\n", " C: float | \u001b[38;5;28;01mNone\u001b[39;00m = \u001b[38;5;28;01mNone\u001b[39;00m,\n", " Ea: float | \u001b[38;5;28;01mNone\u001b[39;00m = \u001b[38;5;28;01mNone\u001b[39;00m,\n", " D: float | \u001b[38;5;28;01mNone\u001b[39;00m = \u001b[38;5;28;01mNone\u001b[39;00m,\n", " E: float | \u001b[38;5;28;01mNone\u001b[39;00m = \u001b[38;5;28;01mNone\u001b[39;00m,\n", " reactants: List[musica.mechanism_configuration.species.Species | Tuple[float, musica.mechanism_configuration.species.Species]] | \u001b[38;5;28;01mNone\u001b[39;00m = \u001b[38;5;28;01mNone\u001b[39;00m,\n", " products: List[musica.mechanism_configuration.species.Species | Tuple[float, musica.mechanism_configuration.species.Species]] | \u001b[38;5;28;01mNone\u001b[39;00m = \u001b[38;5;28;01mNone\u001b[39;00m,\n", " gas_phase: musica.mechanism_configuration.phase.Phase | \u001b[38;5;28;01mNone\u001b[39;00m = \u001b[38;5;28;01mNone\u001b[39;00m,\n", " other_properties: Dict[str, Any] | \u001b[38;5;28;01mNone\u001b[39;00m = \u001b[38;5;28;01mNone\u001b[39;00m,\n", ")\n", "\u001b[31mDocstring:\u001b[39m \n", "An Arrhenius rate constant.\n", "\n", "k = A * exp( C / T ) * ( T / D )^B * exp( 1 - E * P )\n", "\n", "where:\n", " k = rate constant\n", " A = pre-exponential factor [(mol m-3)^(n-1)s-1]\n", " B = temperature exponent [unitless]\n", " C = exponential term [K-1]\n", " D = reference temperature [K]\n", " E = pressure scaling term [Pa-1]\n", " T = temperature [K]\n", " P = pressure [Pa]\n", " n = number of reactants\n", "\n", "Attributes:\n", " name: The name of the Arrhenius rate constant.\n", " A: Pre-exponential factor [(mol m-3)^(n-1)s-1].\n", " B: Temperature exponent [unitless].\n", " C: Exponential term [K-1].\n", " D: Reference Temperature [K].\n", " E: Pressure scaling term [Pa-1].\n", " reactants: A list of reactants involved in the reaction.\n", " products: A list of products formed in the reaction.\n", " gas_phase: The gas phase in which the reaction occurs.\n", " other_properties: A dictionary of other properties.\n", "\u001b[31mInit docstring:\u001b[39m\n", "Initialize the Arrhenius reaction.\n", "\n", "Args:\n", " name: The name of the Arrhenius rate constant.\n", " A: Pre-exponential factor [(mol m-3)^(n-1)s-1].\n", " B: Temperature exponent [unitless].\n", " C: Exponential term [K-1].\n", " Ea: Activation energy [J molecule-1]. Mutually exclusive with C.\n", " D: Reference Temperature [K].\n", " E: Pressure scaling term [Pa-1].\n", " reactants: A list of reactants involved in the reaction.\n", " products: A list of products formed in the reaction.\n", " gas_phase: The gas phase in which the reaction occurs.\n", " other_properties: A dictionary of other properties.\n", "\u001b[31mFile:\u001b[39m ~/Documents/Projects/musica/venv/lib/python3.14/site-packages/musica/mechanism_configuration/arrhenius.py\n", "\u001b[31mType:\u001b[39m type\n", "\u001b[31mSubclasses:\u001b[39m " ] } ], "source": [ "mc.Arrhenius?" ] }, { "cell_type": "markdown", "id": "8ccc93f7", "metadata": {}, "source": [ "In the last chapter, we used the `musica` parser to turn our configuration file into a `mechanism`. Here, we'll create the mechanism out of the chemistry objects we've defined above" ] }, { "cell_type": "code", "execution_count": 4, "id": "80008904-7666-449e-a527-8058e5194512", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[Arrhenius] A -> B\n", "[Arrhenius] B -> C\n" ] } ], "source": [ "mechanism = mc.Mechanism(\n", " name=\"musica_micm_example\",\n", " species=species,\n", " phases=[gas],\n", " reactions=[r1, r2]\n", ")\n", "for reaction in mechanism.reactions:\n", " print(f\"[{reaction.type.name}] {reaction.to_equation()}\")" ] }, { "cell_type": "markdown", "id": "7861e510", "metadata": {}, "source": [ "## 3. Creating the Solver\n", "\n", "We create a solver for our `mechanism` the same way we did in the last tutorial.\n", "One minor difference here, is that we specifcy the `solver_type`.\n", "For more information on the types of solvers available, see [here](https://ncar.github.io/micm/user_guide/solver_configurations.html).\n", "The standard-ordered Rosenbrock solver is actually the default `solver_type`, so including it here has no real effect.\n", "\n", ">__NOTE:__ If you're wondering what \"standard order\" means, it refers to how the matrix data in the `state` in arranged in memory. `micm` is designed to be performant enough to be included in large-scale climate and weather models. To achieve optimal performance we have advanced matrix ordering strategies we can apply to encourage vectorization of the solver instructions. For most Python-based applications, the standard ordering will be sufficient." ] }, { "cell_type": "code", "execution_count": 5, "id": "18bb7d49", "metadata": {}, "outputs": [], "source": [ "solver = musica.MICM(mechanism = mechanism, solver_type = musica.SolverType.rosenbrock_standard_order)" ] }, { "cell_type": "markdown", "id": "2199a26f", "metadata": {}, "source": [ "## 4. Creating the State\n", "\n", "In the last tutorial, we created a single-grid-cell `state`. This time, let's double the complexity of our system and create a two-grid-cell `state`!" ] }, { "cell_type": "code", "execution_count": 6, "id": "d91a4be8", "metadata": {}, "outputs": [], "source": [ "num_grid_cells = 2\n", "state = solver.create_state(num_grid_cells)" ] }, { "cell_type": "markdown", "id": "08d6e59c", "metadata": {}, "source": [ "## 5. Setting Conditions\n", "\n", "Next, let's set the conditions for our two-grid-cell state. In the last tutorial, you may have wondered why the temperature, pressure, and species concentrations were in array format. Hopefully, now it's clear - one element for each grid cell!" ] }, { "cell_type": "code", "execution_count": 7, "id": "e0a74a68", "metadata": {}, "outputs": [], "source": [ "state.set_conditions(temperatures=[300, 100], pressures=[101253.3, 11253.3])\n", "state.set_concentrations({\n", " 'A': [5, 20],\n", " 'B': [5, 3],\n", " 'C': [5, 7]\n", "})" ] }, { "cell_type": "markdown", "id": "1238f02b", "metadata": {}, "source": [ "## 7. Solving the System\n", "\n", "This time around, we'll advance the state for 60 s, collecting intermediate states each second." ] }, { "cell_type": "code", "execution_count": 8, "id": "76b21308", "metadata": {}, "outputs": [], "source": [ "concentrations_solved = []\n", "time_step = 10 # s\n", "sim_length = 600 # s\n", "curr_time = 0 # s\n", "while curr_time <= sim_length:\n", " solver.solve(state, time_step)\n", " concentrations_solved.append(state.get_concentrations())\n", " curr_time += time_step" ] }, { "cell_type": "markdown", "id": "5d2185c0", "metadata": {}, "source": [ "## 8. Preparing the Results (Advanced; Optional Read)\n", "\n", "We're going to create a short helper function to put the results into a Pandas DataFrame for easier plotting.\n", "The function will extract results for a single grid cell, and we call it twice, once for each of the grid cells in our `state`.\n", "In this simulation, the temperature, pressure, and air density are all constant, so numpy's `repeat()` function is used to repeat their respective values for every time step." ] }, { "cell_type": "code", "execution_count": 9, "id": "65de1f01", "metadata": {}, "outputs": [], "source": [ "def convert_results_single_cell(cell_index):\n", " concentrations_solved_pd = []\n", " for i in range(0, sim_length + 1, time_step):\n", " concentrations_solved_pd.append({species: concentration[cell_index] for species, concentration in concentrations_solved[int(i/time_step)].items()})\n", " df = pd.DataFrame(concentrations_solved_pd)\n", " df = df.rename(columns = {'A' : 'CONC.A.mol m-3', 'B' : 'CONC.B.mol m-3', 'C' : 'CONC.C.mol m-3'})\n", " df['time.s'] = list(map(float, range(0, sim_length + 1, time_step)))\n", " df['ENV.temperature.K'] = np.repeat(state.get_conditions()['temperature'][cell_index], sim_length/time_step + 1.0)\n", " df['ENV.pressure.Pa'] = np.repeat(state.get_conditions()['pressure'][cell_index], sim_length/time_step + 1.0)\n", " df['ENV.air number density.mol m-3'] = np.repeat(state.get_conditions()['air_density'][cell_index], sim_length/time_step + 1.0)\n", " df = df[['time.s', 'ENV.temperature.K', 'ENV.pressure.Pa', 'ENV.air number density.mol m-3', 'CONC.A.mol m-3', 'CONC.B.mol m-3', 'CONC.C.mol m-3']]\n", " return df" ] }, { "cell_type": "code", "execution_count": 10, "id": "e9dda1ed", "metadata": {}, "outputs": [], "source": [ "df_0 = convert_results_single_cell(0)\n", "df_1 = convert_results_single_cell(1)" ] }, { "cell_type": "markdown", "id": "ada2398b", "metadata": {}, "source": [ "## 9. Viewing the Results\n", "\n", "Finally, let's desplay and plot the DataFrames to show the evolution of both of the independent systems over time." ] }, { "cell_type": "code", "execution_count": 11, "id": "d49f1b08", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
| \n", " | time.s | \n", "ENV.temperature.K | \n", "ENV.pressure.Pa | \n", "ENV.air number density.mol m-3 | \n", "CONC.A.mol m-3 | \n", "CONC.B.mol m-3 | \n", "CONC.C.mol m-3 | \n", "
|---|---|---|---|---|---|---|---|
| 0 | \n", "0.0 | \n", "300.0 | \n", "101253.3 | \n", "40.59324282282551 | \n", "4.76922334303805 | \n", "4.994590692532435 | \n", "5.236185964429513 | \n", "
| 1 | \n", "10.0 | \n", "300.0 | \n", "101253.3 | \n", "40.59324282282551 | \n", "4.549098259155805 | \n", "4.9790291487427 | \n", "5.471872592101488 | \n", "
| 2 | \n", "20.0 | \n", "300.0 | \n", "101253.3 | \n", "40.59324282282551 | \n", "4.339133121467923 | \n", "4.954264051821206 | \n", "5.706602826710863 | \n", "
| 3 | \n", "30.0 | \n", "300.0 | \n", "101253.3 | \n", "40.59324282282551 | \n", "4.138858994290876 | \n", "4.921178138841699 | \n", "5.939962866867419 | \n", "
| 4 | \n", "40.0 | \n", "300.0 | \n", "101253.3 | \n", "40.59324282282551 | \n", "3.9478285858230064 | \n", "4.8805922672973425 | \n", "6.171579146879646 | \n", "
| ... | \n", "... | \n", "... | \n", "... | \n", "... | \n", "... | \n", "... | \n", "... | \n", "
| 56 | \n", "560.0 | \n", "300.0 | \n", "101253.3 | \n", "40.59324282282551 | \n", "0.3382177774161425 | \n", "1.2492099030181651 | \n", "13.412572319565694 | \n", "
| 57 | \n", "570.0 | \n", "300.0 | \n", "101253.3 | \n", "40.59324282282551 | \n", "0.32260722381670276 | \n", "1.2067968547753385 | \n", "13.470595921407961 | \n", "
| 58 | \n", "580.0 | \n", "300.0 | \n", "101253.3 | \n", "40.59324282282551 | \n", "0.3077171804918639 | \n", "1.1656377730117506 | \n", "13.526645046496386 | \n", "
| 59 | \n", "590.0 | \n", "300.0 | \n", "101253.3 | \n", "40.59324282282551 | \n", "0.29351439205113017 | \n", "1.1257072563871726 | \n", "13.580778351561698 | \n", "
| 60 | \n", "600.0 | \n", "300.0 | \n", "101253.3 | \n", "40.59324282282551 | \n", "0.27996713801757456 | \n", "1.086979577032808 | \n", "13.633053284949622 | \n", "
61 rows × 7 columns
\n", "| \n", " | time.s | \n", "ENV.temperature.K | \n", "ENV.pressure.Pa | \n", "ENV.air number density.mol m-3 | \n", "CONC.A.mol m-3 | \n", "CONC.B.mol m-3 | \n", "CONC.C.mol m-3 | \n", "
|---|---|---|---|---|---|---|---|
| 0 | \n", "0.0 | \n", "100.0 | \n", "11253.3 | \n", "13.53460893002309 | \n", "18.72357316215889 | \n", "4.04334124172365 | \n", "7.233085596117464 | \n", "
| 1 | \n", "10.0 | \n", "100.0 | \n", "11253.3 | \n", "13.53460893002309 | \n", "17.528609597935834 | \n", "4.941288116208338 | \n", "7.530102285855836 | \n", "
| 2 | \n", "20.0 | \n", "100.0 | \n", "11253.3 | \n", "13.53460893002309 | \n", "16.40991021189361 | \n", "5.708149451042962 | \n", "7.881940337063443 | \n", "
| 3 | \n", "30.0 | \n", "100.0 | \n", "11253.3 | \n", "13.53460893002309 | \n", "15.362607721842412 | \n", "6.356999871717215 | \n", "8.28039240644039 | \n", "
| 4 | \n", "40.0 | \n", "100.0 | \n", "11253.3 | \n", "13.53460893002309 | \n", "14.382145482073177 | \n", "6.899779056293003 | \n", "8.718075461633836 | \n", "
| ... | \n", "... | \n", "... | \n", "... | \n", "... | \n", "... | \n", "... | \n", "... | \n", "
| 56 | \n", "560.0 | \n", "100.0 | \n", "11253.3 | \n", "13.53460893002309 | \n", "0.4660987829533735 | \n", "1.822029649269642 | \n", "27.711871567777017 | \n", "
| 57 | \n", "570.0 | \n", "100.0 | \n", "11253.3 | \n", "13.53460893002309 | \n", "0.4363517331710352 | \n", "1.7345223337021152 | \n", "27.829125933126896 | \n", "
| 58 | \n", "580.0 | \n", "100.0 | \n", "11253.3 | \n", "13.53460893002309 | \n", "0.4085031800231355 | \n", "1.6507632617514378 | \n", "27.940733558225475 | \n", "
| 59 | \n", "590.0 | \n", "100.0 | \n", "11253.3 | \n", "13.53460893002309 | \n", "0.38243195890688714 | \n", "1.5706304291624618 | \n", "28.046937611930705 | \n", "
| 60 | \n", "600.0 | \n", "100.0 | \n", "11253.3 | \n", "13.53460893002309 | \n", "0.3580246381070421 | \n", "1.4940021374208303 | \n", "28.147973224472178 | \n", "
61 rows × 7 columns
\n", "